#!/usr/bin/env python
# -*- coding: utf-8 -*-

# Projet CellMates
# Judith ROBSON
# Roxane LEVY
# Ludovic JURADO


import sys
import copy
import Globals
import Widgets
import Options
import autoCell
import huffman
import transRegles
from PySide.QtCore import *
from PySide.QtGui import *
from bitarray import bitarray


""" Classe "MAIN".
    Elle crée l'application et gère les interactions entre l'utilisateur et l'application.

Lors de son initialisation, la fenêtre est créée, ainsi que le menu, l'automate et les
différentes options. Les valeurs de départ sont initialisées, puis les boutons sont connectés
à leur(s) fonction(s).

Dans cette classe, on trouve :
- une fonction d'actualisation pour chaque option
- une fonction de connexion qui associe, à chaque bouton, une ou plusieurs fonction(s)
- une fonction d'initialisation des variables globales
- une ou plusieurs fonction(s) exécutant l'action de chaque bouton
- plusieurs fonctions de traduction (entre seuil et densité, entre syntaxe interne et syntaxe externe des règles, entre configuration compressé et variables globales)
- une fonction d'actualisation de l'automate
- le moteur du mini-jeu
"""

class Main :

    # Fonction d'initialisation
    def __init__(self) :
        self.application = QApplication(sys.argv) # Création de l'application
        self.mainWindow = QMainWindow() # Création de la fenêtre principale
        self.mainWindow.setWindowTitle(qApp.trUtf8("CellMates")) # Titre de la fenêtre principale
        self.mainWindow.resize(800, 600)
        window = QWidget() # Création du widget central
        mainLayout = QHBoxLayout() # Création du layout principal

        menu = QMenuBar(self.mainWindow) # Création du menu
        fichier = QMenu(qApp.trUtf8("&Fichier")) # Menu
        self.new = fichier.addAction(qApp.trUtf8("&Nouveau"))
        self.save = fichier.addAction(qApp.trUtf8("&Sauvegarder"))
        self.load = fichier.addAction(qApp.trUtf8("&Charger"))
        self.quit = fichier.addAction(qApp.trUtf8("&Quitter"))
        menu.addMenu(fichier) # Attachement du premier menu
        self.mainWindow.setMenuBar(menu) # Attachement de la barre de menu à la fenêtre principale

        # Création de la grille
        self.grille = Widgets.Grille()
        grilleView = Widgets.GrilleView(self.grille)
        mainLayout.addWidget(grilleView)

        self.initGlobals() # Initialisation des variables globales

        self.numGlobals = 21

        self.autoCell = autoCell.AutoCell(Globals.grilleLignes, Globals.grilleColonnes,
                                              Globals.grille, Globals.nombreEtats,
                                              Globals.torus, Globals.tailleVoisinage,
                                            Globals.regles, Globals.poidsVoisinage,
                                               self.densiteToSeuil(Globals.densite)) # Création de l'automate

        self.transRegles = transRegles.TransRegles() # objet permettant traduction syntaxe interne vers utilisateur et vice versa
        self.huffMan = huffman.Huffman()   # objet permettant la compression et la décompression d'une partie de jeu
        # Création des options
        optionsArea = QScrollArea()
        optionsArea.setFixedWidth(630)
        self.options = Options.Options()
        self.resetOptions() # Initialisation des options
        optionsArea.setWidget(self.options)
        mainLayout.addWidget(optionsArea)

        self.timer = QTimer() # Création du timer permettant le défilement automatique
        self.timer.setInterval(5000/Globals.vitesse) # Initialisation de la vitesse de défilement

        self.modeArr = False

        self.connexions() # Connexion des boutons

        window.setLayout(mainLayout) # Attachement du layout principal au widget central
        self.mainWindow.setCentralWidget(window) # Attachement du widget central à la fenêtre principale

        self.mainWindow.show() # Affichage de l'application
        self.application.exec_() # Boucle principale


    # Fonction d'initialisation des variables globales
    def initGlobals(self) :
        Globals.statutJDLM = False
        Globals.pointsJDLM = 0
        Globals.pointsMaxJDLM = [0, 1]
        Globals.difficulteJDLM = 1
        Globals.limiteJDLM = 1000
        Globals.nombreEtats = 2
        Globals.colors = [QColor("white"), QColor("black"),  QColor("red"), QColor(0,255,0), QColor("blue")]
        Globals.selectedState = 0
        Globals.densite = 2 * [50] + 3 * [0]
        Globals.tailleVoisinage = 5 * [1]
        Globals.typeVoisinage = 5 * [1]
        Globals.totalistique = 5 * [True]
        Globals.poidsVoisinage = []
        for i in range(5) :
            Globals.poidsVoisinage.append([])
            taille = Globals.tailleVoisinage[i]
            diametre = (taille + 1) * 2 - 1
            for j in range(diametre) :
                Globals.poidsVoisinage[i].append([])
                for k in range(diametre) :
                    if j == taille and k == taille :
                        Globals.poidsVoisinage[i][j].append(0)
                    else :
                        Globals.poidsVoisinage[i][j].append(1)
        Globals.torus = False
        Globals.grilleLignes = 20
        Globals.grilleColonnes = 20
        Globals.regles = {0 : {1 : {1 : [3]}}, 1 : {1 : {1 : [2, 3]}}}
        Globals.vitesse = 5
        Globals.etape = 0
        self.grille.initGrille()


    # fonction mettant à jour les options du "Jeu de la mort"
    def resetJDLM(self) :
        self.options.valDifficulte.setValue(Globals.difficulteJDLM)
        self.options.valLimEtape.setValue(Globals.limiteJDLM)
        self.options.scoreJDLM.setText(str(int(Globals.pointsMaxJDLM[0])) + " (" + str(Globals.pointsMaxJDLM[1]) + ") " + " / -")


    # Fonction mettant à jour le nombre d'états sélectionnés par l'utilisateur
    def resetNombreEtats(self) :
        for i in range(4) :
            if i == Globals.nombreEtats - 2 :
                self.options.etats[i].setChecked(True)
            else :
                self.options.etats[i].setChecked(False)

        for i in range(Globals.nombreEtats) :
            self.options.couleur[i].setEnabled(True)
        for i in range(Globals.nombreEtats, 5) :
            self.options.couleur[i].setEnabled(False)


    # Fonction mettant à jour les couleurs correspondant aux états
    def resetCouleurs(self) :
        for i in range(5) :
            self.options.couleur[i].setColor(Globals.colors[i])


    # Fonction mettant à jour l'état sélectionné
    def resetSelectedColor(self) :
        for i in range(5) :
            if i == Globals.selectedState :
                self.options.couleur[i].setSize(QSize(30, 30))
            else :
                self.options.couleur[i].setSize(QSize(20, 20))


    # Fonction ré-initialisant les densités de couleur (à un pourcentage équitable)
    def resetSeuils(self) :
        self.options.eqDensite.setChecked(False)

        seuil = 100 // Globals.nombreEtats
        gap = 100 % Globals.nombreEtats

        for i in range(5) :
            self.options.densites[i].setValue(0)

        self.options.densites[0].setValue(seuil+gap)

        for i in range(1, Globals.nombreEtats) :
            self.options.densites[i].setValue(seuil)

        self.setMaxDensite()


    # Fonction mettant à jour la taille du voisinage
    def resetTailleVoisinage(self) :
        for i in range(3) :
            if i == Globals.tailleVoisinage[Globals.selectedState] - 1 :
                self.options.taille[i].setChecked(True)
            else :
                self.options.taille[i].setChecked(False)


    # Fonction mettant à jour le voisinage
    def resetVoisinage(self) :
        if Globals.typeVoisinage[Globals.selectedState] == 1 :
            self.options.moore.setChecked(True)
            self.options.vonNeumann.setChecked(False)
            self.options.voisinagePerso.setChecked(False)
        elif Globals.typeVoisinage[Globals.selectedState] == 2 :
            self.options.moore.setChecked(False)
            self.options.vonNeumann.setChecked(True)
            self.options.voisinagePerso.setChecked(False)
        else :
            self.options.moore.setChecked(False)
            self.options.vonNeumann.setChecked(False)
            self.options.voisinagePerso.setChecked(True)

        self.options.nonTotalistique.setChecked(not Globals.totalistique[Globals.selectedState])

        if Globals.typeVoisinage[Globals.selectedState] :
            self.options.aleatoire.setEnabled(False)
            self.options.nonTotalistique.setEnabled(False)
            poids = self.autoCell.genPoids(Globals.typeVoisinage[Globals.selectedState], True, Globals.tailleVoisinage[Globals.selectedState])
            Globals.poidsVoisinage[Globals.selectedState] = poids
        else :
            self.options.aleatoire.setEnabled(True)
            self.options.nonTotalistique.setEnabled(True)

        self.options.voisinageConfig.update()


    # Fonction mettant à jour le type de grille
    def resetTypeGrille(self) :
        self.options.toroidale.setChecked(Globals.torus)
        self.options.bornee.setChecked(not Globals.torus)


    # Fonction mettant à jour la taille de la grille
    def resetTailleGrille(self) :
        self.options.ligne.setValue(Globals.grilleLignes)
        self.options.colonne.setValue(Globals.grilleColonnes)


    # Fonction mettant à jour l'affichage des règles
    def resetRegles(self) :
        self.options.listeRegles.setText(qApp.trUtf8(self.reglesIntToExt(Globals.regles)))


    # Fonction mettant à jour la vitesse de défilement
    def resetVitesse(self) :
        self.options.vitesseSlider.setValue(Globals.vitesse)


    # Fonction ré-initialisant l'étape courante
    def resetEtape(self) :
        Globals.etape = 0
        self.options.numEtape.setValue(0)


    # Fonction remettant toutes les options à jour
    def resetOptions(self) :
        self.resetJDLM()
        self.resetNombreEtats()
        self.resetCouleurs()
        self.resetSelectedColor()
        self.resetSeuils()
        self.resetTypeGrille()
        self.resetTailleGrille()
        self.resetTailleVoisinage()
        self.resetVoisinage()
        self.resetRegles()
        self.resetVitesse()
        self.resetEtape()


    # Fonction ré-initialisant toutes les options
    def resetAll(self) :
        self.initGlobals()
        self.resetOptions()


    # affiche un message d'erreur dans un QMessageBox
    def afficheErreur(self, message) :
        error = QMessageBox()
        error.setInformativeText(qApp.trUtf8(message))
        error.setWindowTitle(qApp.trUtf8("Erreur"))
        spacer = QSpacerItem(300, 0, QSizePolicy.Minimum, QSizePolicy.Expanding)
        layout = error.layout()
        layout.addItem(spacer, layout.rowCount(), 0, 1, layout.columnCount())
        error.exec_()


    # sauvegarde l'automate et la configuration courante dans un fichier après compression
    def saveAutomate(self) :
        # ouvre une boîte de dialogue avec le répertoire courant
        filename = QFileDialog.getSaveFileName(self.mainWindow, "Sauvegarde de la configuration")
        # récupère toutes les globales et les met dans une liste
        liste = []
        liste.append(Globals.statutJDLM)
        liste.append(Globals.pointsJDLM)
        liste.append(Globals.pointsMaxJDLM)
        liste.append(Globals.difficulteJDLM)
        liste.append(Globals.limiteJDLM)
        liste.append(Globals.nombreEtats)
        liste.append([])
        for x in range(len(Globals.colors)) :
            liste[len(liste) - 1].append([Globals.colors[x].red(), Globals.colors[x].green(), Globals.colors[x].blue()])
        liste.append(Globals.selectedState)
        liste.append(Globals.densite)
        liste.append(Globals.tailleVoisinage)
        liste.append(Globals.typeVoisinage)
        liste.append(Globals.totalistique)
        liste.append(Globals.poidsVoisinage)
        liste.append(Globals.torus)
        liste.append(Globals.grilleLignes)
        liste.append(Globals.grilleColonnes)
        liste.append(Globals.regles)
        liste.append(Globals.vitesse)
        liste.append(Globals.etape)
        liste.append(Globals.grille)
        liste.append(Globals.grillesAnte)

        if filename :
            if filename == (u'', u'') :   # si l'utilisateur décide d'annuler la sauvegarde
                return
            try : fichier = open((filename[0]+'.cmc' if (filename[0][-4:] != ".cmc") else filename[0]), 'w')
            except :
                self.afficheErreur("Erreur à l'ouverture du fichier " + filename[0])
                return
            self.huffMan.__init__()  # initialisation
            self.huffMan.matrice = str(liste)
            self.huffMan.encode()
            fichier.write(repr(self.huffMan.prefix)+'\n')
            fichier.write(str(self.huffMan.huff.buffer_info()[3]))
            self.huffMan.huff.tofile(fichier)
            fichier.close()
            QMessageBox.information(self.mainWindow, self.mainWindow.trUtf8("Sauvegarde"), self.mainWindow.trUtf8("La sauvegarde est terminée"))


    # vérifie et extrait les données issues de la décompression
    def extractMatrice(self, matrice) :
        if len(matrice) is not self.numGlobals : return False

        # pour chaque entrée on vérifie qu'elle correspond au type et aux valeurs qu'on attend pour les globales

        if type(matrice[0]) is bool : Globals.statutJDLM = matrice[0]
        else : self.resetAll(); return False

        if (type(matrice[1]) is int or type(matrice[1]) is float) and matrice[1] >= 0 : Globals.pointsJDLM = matrice[1]
        else : self.resetAll(); return False

        if type(matrice[2]) is list and len(matrice[2]) is 2 and (type(matrice[2][0]) is int or type(matrice[2][0]) is float) and matrice[2][0] >= 0 and matrice[2][1] in range(1, 11) :
            Globals.pointsMaxJDLM = matrice[2]
        else : self.resetAll(); return False

        if matrice[3] in range(1, 11) : Globals.difficulteJDLM = matrice[3]
        else : self.resetAll(); return False

        if matrice[4] in range(100, 10001) : Globals.limiteJDLM = matrice[4]
        else : self.resetAll(); return False

        if matrice[5] in range(2, 6) : Globals.nombreEtats = matrice[5]
        else : self.resetAll(); return False

        if type(matrice[6]) is list and len(matrice[6]) is 5 :
            valide = True
            couleurs = []
            for i in range(5) :
                if type(matrice[6][i]) is list and len(matrice[6][i]) is 3 :
                    for j in range(3) :
                        if matrice[6][i][j] not in range(0, 256) : valide = False
                else :
                    valide = False
                if valide : couleurs.append(QColor(matrice[6][i][0], matrice[6][i][1], matrice[6][i][2]))
            if valide : Globals.colors = couleurs
            else : self.resetAll(); return False
        else : self.resetAll(); return False

        if matrice[7] in range(0, 5) : Globals.selectedState = matrice[7]
        else : self.resetAll(); return False

        if type(matrice[8]) is list and len(matrice[8]) is 5 :
            valide = True
            for i in range(5) :
                if matrice[8][i] not in range(0, 101) : valide = False
            if valide : Globals.densite = matrice[8]
            else : self.resetAll(); return False
        else : self.resetAll(); return False

        if type(matrice[9]) is list and len(matrice[9]) is 5 :
            valide = True
            for i in range(5) :
                if matrice[9][i] not in range(1, 4) : valide = False
            if valide : Globals.tailleVoisinage = matrice[9]
            else : self.resetAll(); return False
        else : self.resetAll(); return False

        if type(matrice[10]) is list and len(matrice[10]) is 5 :
            valide = True
            for i in range(5) :
                if matrice[10][i] not in range(0, 3) : valide = False
            if valide : Globals.typeVoisinage = matrice[10]
            else : self.resetAll(); return False
        else : self.resetAll(); return False

        if type(matrice[11]) is list and len(matrice[11]) is 5 :
            valide = True
            for i in range(5) :
                if type(matrice[11][i]) is not bool : valide = False
            if valide : Globals.totalistique = matrice[11]
            else : self.resetAll(); return False
        else : self.resetAll(); return False

        if type(matrice[12]) is list and len(matrice[12]) is 5 :
            valide = True
            for i in range(5) :
                diametre = (Globals.tailleVoisinage[i] + 1) * 2 - 1
                if type(matrice[12][i]) is list and len(matrice[12][i]) is diametre :
                    for j in range(diametre) :
                        if type(matrice[12][i][j]) is list and len(matrice[12][i][j]) is diametre :
                            for k in range(diametre) :
                                if type(matrice[12][i][j][k]) is not int or matrice[12][i][j][k] < 0 : valide = False
                        else : valide = False
                else : valide = False
            if valide : Globals.poidsVoisinage = matrice[12]
            else : self.resetAll(); return False
        else : self.resetAll(); return False

        if type(matrice[13]) is bool : Globals.torus = matrice[13]
        else : self.resetAll(); return False

        if matrice[14] in range(2, 101) : Globals.grilleLignes = matrice[14]
        else : self.resetAll(); return False

        if matrice[15] in range(2, 101) : Globals.grilleColonnes = matrice[15]
        else : self.resetAll(); return False

        if type(matrice[16]) is dict and len(matrice[16]) is Globals.nombreEtats :
            valide = True
            for cleP1, valeurP1 in matrice[16].items() :
                if cleP1 in range(0, Globals.nombreEtats) and type(valeurP1) is dict and len(valeurP1) is Globals.nombreEtats - 1 :
                    maxVal = pow((Globals.tailleVoisinage[cleP1] + 1) * 2 - 1, 2) + 1
                    for cleP2, valeurP2 in valeurP1.items() :
                        if cleP2 in range(1, Globals.nombreEtats) and type(valeurP2) is dict and len(valeurP2) is Globals.nombreEtats - 1 :
                            for cleP3, valeurP3 in valeurP2.items() :
                                if cleP3 in range(1, Globals.nombreEtats) and type(valeurP3) is list :
                                    for val in valeurP3 :
                                        if val not in range(0, maxVal) : valide = False
                                else : valide = False
                        else : valide = False
                else : valide = False
            if valide : Globals.regles = matrice[16]
            else : self.resetAll(); return False
        else : self.resetAll(); return False

        if matrice[17] in range(1, 31) : Globals.vitesse = matrice[17]
        else : self.resetAll(); return False

        if matrice[18] in range(0, 10001) :
            Globals.etape = matrice[18]
            self.options.numEtape.setValue(Globals.etape)
        else : self.resetAll(); return False

        if type(matrice[19]) is list and len(matrice[19]) is Globals.grilleLignes :
            valide = True
            for i in range(Globals.grilleLignes) :
                if type(matrice[19][i]) is list and len(matrice[19][i]) is Globals.grilleColonnes :
                    for j in range(Globals.grilleColonnes) :
                        if matrice[19][i][j] not in range(0, Globals.nombreEtats) : valide = False
                else : valide = False
            if valide : Globals.grille = matrice[19]
            else : self.resetAll(); return False
        else : self.resetAll(); return False

        if type(matrice[20]) is list and len(matrice[20]) > 0 :
            # on suppose que si la première est valide les suivantes aussi (gain de temps)
            if type(matrice[20][0]) is list and len(matrice[20][0]) is Globals.grilleLignes :
                valide = True
                for i in range(Globals.grilleLignes) :
                    if type(matrice[20][0][i]) is list and len(matrice[20][0][i]) is Globals.grilleColonnes :
                        for j in range(Globals.grilleColonnes) :
                            if matrice[20][0][i][j] not in range(0, Globals.nombreEtats) : valide = False
                    else : valide = False
                if valide : Globals.grillesAnte = matrice[20]
                else : self.resetAll(); return False
            else : self.resetAll(); return False
        else : self.resetAll(); return False

        # on met à jour les options, la grille et l'automate

        self.resetJDLM()
        self.resetNombreEtats()
        self.resetCouleurs()
        self.resetSelectedColor()
        for i in range(5) :
            self.options.densites[i].setValue(Globals.densite[i])
        self.resetTypeGrille()
        self.resetTailleGrille()
        self.resetTailleVoisinage()
        self.resetVoisinage()
        self.resetRegles()
        self.resetVitesse()

        self.grille.update()
        self.autoCellUpdate()

        return True


    # charge un fichier contenant un automate et une configuration, le décompresse, et met à jour les données du programme
    def loadAutomate(self) :
        filename = QFileDialog.getOpenFileName(self.mainWindow, "Chargement de la configuration")
        if filename :
            if filename == (u'', u'') :   # si l'utilisateur décide d'annuler le chargement d'un fichier
                return
            if (filename[0][-4:] != ".cmc" ) :
                self.afficheErreur("Fichier non conforme")
                return
            try : fichier = open(filename[0], "r")
            except :
                self.afficheErreur("Erreur à l'ouverture du fichier " + filename[0])
                return
            self.huffMan.__init__()  # initialisation
            try : self.huffMan.prefix = eval(fichier.readline())   # récupère bien un codage préfixé
            except :
                self.afficheErreur("Fichier non conforme")
                fichier.close()
                return
            try : unused_bytes = int(fichier.read(1))      # récupère bien le nombre de unused_bytes
            except :
                self.afficheErreur("Fichier non conforme")
                fichier.close()
                return
            try : self.huffMan.huff.fromfile(fichier)  # récupère un bitarray()
            except :
                self.afficheErreur("Fichier non conforme")
                fichier.close()
                return
            fichier.close()
            if unused_bytes : del self.huffMan.huff[-unused_bytes:]
            self.huffMan.decode()
            try : m = eval(self.huffMan.matrice) # remet toutes les globales dans la matrice
            except :
                self.afficheErreur("Fichier non conforme")
                return
            if self.extractMatrice(m) :
                QMessageBox.information(self.mainWindow, self.mainWindow.trUtf8("Chargement"), self.mainWindow.trUtf8("Le chargement est terminé"))
            else : self.afficheErreur("Fichier non conforme")


    # Fonction connectant les boutons à leur(s) fonction(s)
    def connexions(self) :
        self.new.triggered.connect(lambda: self.resetAll())
        self.quit.triggered.connect(lambda: self.application.quit())
        self.save.triggered.connect(lambda: self.saveAutomate())
        self.load.triggered.connect(lambda: self.loadAutomate())

        self.options.activeJDLM.clicked.connect(lambda: self.alterneJDLM())
        self.options.valDifficulte.valueChanged.connect(lambda: self.changeDiffJDLM())
        self.options.valLimEtape.valueChanged.connect(lambda: self.changeLimiteJDLM())

        self.options.etats[0].pressed.connect(lambda: self.numberEtats(2))
        self.options.etats[1].pressed.connect(lambda: self.numberEtats(3))
        self.options.etats[2].pressed.connect(lambda: self.numberEtats(4))
        self.options.etats[3].pressed.connect(lambda: self.numberEtats(5))

        self.options.couleur[0].doubleClicked.connect(lambda: self.colorPick(0))
        self.options.couleur[0].clicked.connect(lambda: self.selectColor(0))
        self.options.couleur[1].doubleClicked.connect(lambda: self.colorPick(1))
        self.options.couleur[1].clicked.connect(lambda: self.selectColor(1))
        self.options.couleur[2].doubleClicked.connect(lambda: self.colorPick(2))
        self.options.couleur[2].clicked.connect(lambda: self.selectColor(2))
        self.options.couleur[3].doubleClicked.connect(lambda: self.colorPick(3))
        self.options.couleur[3].clicked.connect(lambda: self.selectColor(3))
        self.options.couleur[4].doubleClicked.connect(lambda: self.colorPick(4))
        self.options.couleur[4].clicked.connect(lambda: self.selectColor(4))

        self.options.densites[0].valueChanged.connect(lambda: self.setDensite(self.options.densites[0].value(), 0))
        self.options.densites[1].valueChanged.connect(lambda: self.setDensite(self.options.densites[1].value(), 1))
        self.options.densites[2].valueChanged.connect(lambda: self.setDensite(self.options.densites[2].value(), 2))
        self.options.densites[3].valueChanged.connect(lambda: self.setDensite(self.options.densites[3].value(), 3))
        self.options.densites[4].valueChanged.connect(lambda: self.setDensite(self.options.densites[4].value(), 4))
        self.options.newDensite.clicked.connect(lambda: self.genDensite())
        self.options.eqDensite.clicked.connect(lambda: self.genEqDensite())

        self.options.taille[0].pressed.connect(lambda: self.setTailleVois(1))
        self.options.taille[1].pressed.connect(lambda: self.setTailleVois(2))
        self.options.taille[2].pressed.connect(lambda: self.setTailleVois(3))

        self.options.moore.pressed.connect(lambda: self.setTypeVoisinage(1))
        self.options.vonNeumann.pressed.connect(lambda: self.setTypeVoisinage(2))
        self.options.voisinagePerso.pressed.connect(lambda: self.setTypeVoisinage(0))
        self.options.aleatoire.clicked.connect(lambda: self.genRandomVois())
        self.options.nonTotalistique.stateChanged.connect(lambda: self.setTotalistique())

        self.options.bornee.pressed.connect(lambda: self.setType(False))
        self.options.toroidale.pressed.connect(lambda: self.setType(True))

        self.options.ligne.editingFinished.connect(lambda: self.setLigne())
        self.options.colonne.editingFinished.connect(lambda: self.setColonne())

        self.options.mono.clicked.connect(lambda: self.genMonocolore())
        self.options.random.clicked.connect(lambda: self.genRandomGrille())

        self.options.randomRegles.clicked.connect(lambda: self.genRandomRegles())
        self.options.persoRegles.clicked.connect(lambda: self.openChoixRegles())

        self.options.vitesseSlider.valueChanged.connect(lambda: self.setVitesse())

        self.options.numEtape.valueChanged.connect(lambda: self.etapeN())
        self.options.playArriere.clicked.connect(lambda: self.playArriere())
        self.options.playAvant.clicked.connect(lambda: self.playAvant())
        self.options.pause.clicked.connect(lambda: self.pause())

        self.options.gen.clicked.connect(lambda: self.genRandom())

        self.timer.timeout.connect(lambda: self.etapeGrille())


    # fonction qui active/désactive les options
    def alterneOptions(self, enable) :
        self.options.valDifficulte.setEnabled(enable)
        self.options.valLimEtape.setEnabled(enable)

        self.options.etats[0].setEnabled(enable)
        self.options.etats[1].setEnabled(enable)
        self.options.etats[2].setEnabled(enable)
        self.options.etats[3].setEnabled(enable)

        self.options.couleur[0].setEnabled(enable)
        self.options.couleur[1].setEnabled(enable)
        self.options.couleur[2].setEnabled(enable)
        self.options.couleur[3].setEnabled(enable)
        self.options.couleur[4].setEnabled(enable)

        self.options.densites[0].setEnabled(enable)
        self.options.densites[1].setEnabled(enable)
        self.options.densites[2].setEnabled(enable)
        self.options.densites[3].setEnabled(enable)
        self.options.densites[4].setEnabled(enable)
        self.options.newDensite.setEnabled(enable)
        self.options.eqDensite.setEnabled(enable)

        self.options.taille[0].setEnabled(enable)
        self.options.taille[1].setEnabled(enable)
        self.options.taille[2].setEnabled(enable)

        self.options.moore.setEnabled(enable)
        self.options.vonNeumann.setEnabled(enable)
        self.options.voisinagePerso.setEnabled(enable)
        self.options.aleatoire.setEnabled(enable)
        self.options.nonTotalistique.setEnabled(enable)

        self.options.bornee.setEnabled(enable)
        self.options.toroidale.setEnabled(enable)

        self.options.ligne.setEnabled(enable)
        self.options.colonne.setEnabled(enable)

        self.options.mono.setEnabled(enable)
        self.options.random.setEnabled(enable)

        self.options.randomRegles.setEnabled(enable)
        self.options.persoRegles.setEnabled(enable)

        self.options.vitesseSlider.setEnabled(enable)

        self.options.numEtape.setEnabled(enable)
        self.options.playArriere.setEnabled(enable)
        self.options.playAvant.setEnabled(enable)
        self.options.pause.setEnabled(enable)

        self.options.gen.setEnabled(enable)


    # fonction qui active/désactive le mode "Jeu de la Mort", et rend indisponibles les autres fonctionnalités du programme
    def alterneJDLM(self) :
        if self.options.activeJDLM.isChecked() :
            Globals.nombreEtats = 2
            Globals.densite = 2 * [50] + 3 * [0]
            Globals.tailleVoisinage = 5 * [1]
            Globals.typeVoisinage = 5 * [1]
            Globals.totalistique = 5 * [True]
            Globals.poidsVoisinage = []
            for i in range(5) :
                Globals.poidsVoisinage.append([])
                taille = Globals.tailleVoisinage[i]
                diametre = (taille + 1) * 2 - 1
                for j in range(diametre) :
                    Globals.poidsVoisinage[i].append([])
                    for k in range(diametre) :
                        if j == taille and k == taille :
                            Globals.poidsVoisinage[i][j].append(0)
                        else :
                            Globals.poidsVoisinage[i][j].append(1)
            Globals.regles = {0 : {1 : {1 : [3]}}, 1 : {1 : {1 : [2, 3]}}}
            self.resetNombreEtats()
            self.resetCouleurs()
            self.resetSeuils()
            self.resetTailleVoisinage()
            self.resetVoisinage()
            self.resetRegles()
            self.alterneOptions(False)
            self.genRandomGrille()
            self.selectColor(1)
            Globals.pointsJDLM = 0
            Globals.statutJDLM = True
            self.options.scoreJDLM.setText(str(int(Globals.pointsMaxJDLM[0])) + " (" + str(Globals.pointsMaxJDLM[1]) + ") " + " / " + str(int(Globals.pointsJDLM)))
            self.options.vitesseSlider.setValue(self.options.vitesseSlider.maximum())
            self.setVitesse()
            self.playAvant()
        else :
            self.pause()
            Globals.statutJDLM = False
            self.alterneOptions(True)
            self.options.scoreJDLM.setText(str(int(Globals.pointsMaxJDLM[0])) + " (" + str(Globals.pointsMaxJDLM[1]) + ") " + " / -")
            self.resetNombreEtats()
            self.resetCouleurs()
            self.selectColor(0)
            self.genMonocolore()

    # change la difficulté du "Jeu de la Mort"
    def changeDiffJDLM(self) : Globals.difficulteJDLM = self.options.valDifficulte.value()


    # change la limite du nombre d'étapes avant la fin de la partie pour le "Jeu de la Mort"
    def changeLimiteJDLM(self) : Globals.limiteJDLM = self.options.valLimEtape.value()


    # Fonction de sélection du nombre d'états
    def numberEtats(self, nb) :
        Globals.nombreEtats = nb
        Globals.selectedState = 0
        Globals.regles = False
        self.resetNombreEtats()
        self.resetCouleurs()
        self.resetSelectedColor()
        self.resetSeuils()
        self.resetRegles()
        self.genMonocolore()
        self.autoCellUpdate()
        self.setMaxDensite()


    # Fonction de sélection d'un état/couleur
    def selectColor(self, etat) :
        Globals.selectedState = etat
        self.resetSelectedColor()
        self.resetTailleVoisinage()
        self.resetVoisinage()


    # Fonction de personnalisation de couleur
    def colorPick(self, etat) :
        if self.options.couleur[etat].isEnabled() :
            color = QColorDialog.getColor(self.options.couleur[etat].getColor())
            if color.isValid() :
                Globals.colors[Globals.selectedState] = color
                self.resetCouleurs()
                self.grille.update()


    # Fonction de sélection des densités de couleur sur la grille
    def setDensite(self, densite, etat) :
        Globals.densite[etat] = densite
        self.setMaxDensite()


    # Fonction mettant à jour les maximums de densités dynamiquement (afin de toujours avoir un total de 100% maximum)
    def setMaxDensite(self) :
        nb = Globals.nombreEtats
        d1 = self.options.densites[0].value()
        d2 = self.options.densites[1].value()
        d3 = self.options.densites[2].value()
        d4 = self.options.densites[3].value()
        d5 = self.options.densites[4].value()

        self.options.densites[0].setMaximum(100 - d2 - d3 - d4 - d5)
        self.options.densites[1].setMaximum(100 - d1 - d3 - d4 - d5)

        if nb >= 3 :
            self.options.densites[2].setMaximum(100 - d1 - d2 - d4 - d5)
        else :
            self.options.densites[2].setMaximum(0)
        if nb >= 4 :
            self.options.densites[3].setMaximum(100 - d1 - d2 - d3 - d5)
        else :
            self.options.densites[3].setMaximum(0)
        if nb == 5 :
            self.options.densites[4].setMaximum(100 - d1 - d2 - d3 - d4)
        else :
            self.options.densites[4].setMaximum(0)


    # Fonction générant des densités (aléatoires si option d'équilibre non cochée)
    def genDensite(self) :
        self.autoCellUpdate()
        nb = Globals.nombreEtats
        seuils = self.autoCell.genSeuils(self.options.eqDensite.isChecked())
        densites = self.seuilToDensite(seuils)

        for i in range(5) :
            self.options.densites[i].setValue(0)
        for i in range(5) :
            self.options.densites[i].setValue(densites[i])

    # Fonction enclenchant la génération de densités équilibrées
    def genEqDensite(self) :
        if not self.options.eqDensite.isChecked() : return
        self.genDensite()

    # Fonction de sélection de la taille du voisinage (pour l'état courant)
    def setTailleVois(self, taille) :
        etat = Globals.selectedState
        poids = self.autoCell.genPoids(1, True, taille)

        Globals.totalistique[etat] = True
        Globals.poidsVoisinage[etat] = poids
        Globals.typeVoisinage[etat] = 1
        Globals.tailleVoisinage[etat] = taille
        self.resetTailleVoisinage()
        self.resetVoisinage()


    # Fonction de sélection du type de voisinage (pour l'état courant)
    def setTypeVoisinage(self, typeVois) :
        Globals.typeVoisinage[Globals.selectedState] = typeVois
        self.resetVoisinage()


    # Fonction générant un voisinage aléatoire (pour l'état courant)
    def genRandomVois(self) :
        etat = Globals.selectedState
        poids = self.autoCell.genPoids(0, Globals.totalistique[etat], Globals.tailleVoisinage[etat])
        Globals.poidsVoisinage[etat] = poids
        self.resetVoisinage()


    # Fonction permettant de choisir un voisinage non totalistique (pour l'état courant)
    def setTotalistique(self) :
        totalistique = not self.options.nonTotalistique.isChecked()
        Globals.totalistique[Globals.selectedState] = totalistique
        self.resetVoisinage()


    # Fonction de sélection du type de grille
    def setType(self, typeGrille) :
        Globals.torus = typeGrille


    # Fonction de sélection de la hauteur de la grille
    def setLigne(self) :
        self.pause()
        Globals.grilleLignes = self.options.ligne.value()
        self.genMonocolore()


    # Fonction de sélection de la largeur de la grille
    def setColonne(self) :
        self.pause()
        Globals.grilleColonnes = self.options.colonne.value()
        self.genMonocolore()


    # Fonction générant une grille monocolore (pour la couleur sélectionnée)
    def genMonocolore(self) :
        self.resetEtape()
        self.grille.initGrille()


    # Fonction générant une grille aléatoire
    def genRandomGrille(self) :
        self.resetEtape()
        self.autoCellUpdate()
        Globals.grille = self.autoCell.genGrille()
        Globals.grillesAnte = [copy.deepcopy(Globals.grille)]
        self.grille.update()


    # Fonction générant des règles aléatoires
    def genRandomRegles(self) :
        self.autoCellUpdate()
        regles = self.autoCell.genRegles()
        Globals.regles = regles
        self.resetRegles()


    # Fonction permettant de créer ses propres règles
    def openChoixRegles(self) :
        popup = Widgets.ReglesPopup()
        popup.setWindowTitle(qApp.trUtf8("Règles"))
        res = popup.exec_()
        if res :
            regles = self.reglesExtToInt(popup.texteRegles.toPlainText())
            Globals.regles = regles
            self.resetRegles()


    # Fonction de sélection de la vitesse de défilement
    def setVitesse(self) :
        Globals.vitesse = self.options.vitesseSlider.value()
        self.timer.setInterval(5000/Globals.vitesse)


    # Fonction permettant le défilement automatique vers l'arrière
    def playArriere(self) :
        self.modeArr = True
        self.timer.start()


    # Fonction permettant le défilement automatique vers l'avant
    def playAvant(self) :
        self.modeArr = False
        self.timer.start()


    # Fonction mettant en pause le défilement automatique
    def pause(self) :
        self.timer.stop()


    # fonction qui calcul le nombre de points gagnés à chaque étape (mode "Jeu de la Mort") et termine le jeu si fin de partie
    def calculPoints(self) :
        points = 0
        nbEnVie = 0
        for x in range(Globals.grilleLignes) :
            for y in range(Globals.grilleColonnes) :
                if Globals.grille[x][y] : nbEnVie += 1
                if Globals.grille[x][y] > Globals.grillesAnte[-2][x][y] : # si une cellule est venue à la vie
                    # on tient compte de la difficulté comme de la taille de la grille
                    points += (0.1 / Globals.difficulteJDLM) / ((Globals.grilleLignes * Globals.grilleColonnes) / (20.0 * 20.0))
        Globals.pointsJDLM += points
        if Globals.pointsJDLM > Globals.pointsMaxJDLM[0] :
            Globals.pointsMaxJDLM[0] = Globals.pointsJDLM
            Globals.pointsMaxJDLM[1] = Globals.difficulteJDLM
        if not nbEnVie or Globals.etape == Globals.limiteJDLM : # toutes les cellules mortes ou nombre d'étapes limite dépassé, fin de partie
            self.options.activeJDLM.setChecked(False)
            self.alterneJDLM()
        else :
            self.options.scoreJDLM.setText(str(int(Globals.pointsMaxJDLM[0])) + " (" + str(Globals.pointsMaxJDLM[1]) + ") " + " / " + str(int(Globals.pointsJDLM)))


    # Fonction permettant d'avancer d'une étape, vers l'avant ou vers l'arrière
    def etapeGrille(self) :
        if self.modeArr :
            if not Globals.etape : return
            Globals.grillesAnte.pop()
            self.autoCell.grille = copy.deepcopy(Globals.grillesAnte[-1])
            Globals.grille = self.autoCell.grille
            Globals.etape -= 1
        else :
            self.autoCellUpdate()
            self.autoCell.etape()
            Globals.grille = self.autoCell.grille
            Globals.grillesAnte.append(copy.deepcopy(Globals.grille))
            Globals.etape += 1
        self.grille.update()
        self.options.numEtape.setValue(Globals.etape)
        if Globals.statutJDLM :
            self.calculPoints()


    # Fonction permettant de passer à l'étape n
    def etapeN(self) :
        diff = self.options.numEtape.value() - Globals.etape

        if diff > 0 :
            self.autoCellUpdate()
            for i in range(diff) :
                self.autoCell.etape()
                Globals.grillesAnte.append(copy.deepcopy(self.autoCell.grille))
            Globals.grille = self.autoCell.grille
            self.grille.update()
            Globals.etape = self.options.numEtape.value()
        elif diff < 0 :
            Globals.grillesAnte = Globals.grillesAnte[:diff]
            self.autoCell.grille = copy.deepcopy(Globals.grillesAnte[-1])
            Globals.grille = self.autoCell.grille
            self.grille.update()
            Globals.etape = self.options.numEtape.value()


    # Fonction permettant une génération aléatoire globale de l'automate
    def genRandom(self) :
        self.autoCell.__init__(Globals.grilleLignes, Globals.grilleColonnes,
                       False, Globals.nombreEtats,
                       Globals.torus, Globals.tailleVoisinage, False, False,
                       False, self.options.eqDensite.isChecked(), Globals.typeVoisinage, Globals.totalistique)
        Globals.grille = self.autoCell.grille
        Globals.grillesAnte = [copy.deepcopy(Globals.grille)]
        Globals.regles = self.autoCell.regles

        for i in range(Globals.nombreEtats) :
            Globals.poidsVoisinage[i] = self.autoCell.poids[i]

        densites = self.seuilToDensite(self.autoCell.seuils)

        for i in range(5) :
            self.options.densites[i].setValue(0)
        for i in range(5) :
            self.options.densites[i].setValue(densites[i])

        self.resetEtape()
        self.resetRegles()
        self.resetVoisinage()
        self.grille.update()


    # Fonction de conversion de densité vers seuil
    def densiteToSeuil(self, densites) :
        nb = Globals.nombreEtats
        seuils = [densites[0]]

        for i in range(1, nb) :
            seuils.append(densites[i] + seuils[i-1])

        gap = 100 - seuils[nb-1]
        if gap :
            self.options.densites[0].setValue(densites[0] + gap)
            Globals.densite[0] += gap
            for i in range(nb) :
                seuils[i] += gap

        return seuils


    # Fonction de conversion de seuil vers densité
    def seuilToDensite(self, seuils) :
        nb = Globals.nombreEtats
        densites = [seuils[0]]

        for i in range(1, nb) :
            densites.append(seuils[i] - seuils[i-1])

        for i in range(nb, 5) :
            densites.append(0)

        return densites


    # Fonction de conversion de la syntaxe interne des règles vers la syntaxe utilisateur
    def reglesIntToExt(self, regles) : return (self.transRegles.intToExt(regles) if regles else "")


    # Fonction de conversion de la syntaxe utilisateur vers la syntaxe interne des règles
    def reglesExtToInt(self, regles) :
        rules = self.transRegles.extToInt(regles, Globals.nombreEtats, Globals.tailleVoisinage)
        if not rules :
            self.afficheErreur("Syntaxe incorrecte et/ou non respect de la configuration")
            rules = Globals.regles
        return rules


    # Fonction mettant à jour l'automate
    def autoCellUpdate(self) :
        self.autoCell.__init__(Globals.grilleLignes, Globals.grilleColonnes,
                       Globals.grille, Globals.nombreEtats,
                       Globals.torus, Globals.tailleVoisinage,
                       Globals.regles, Globals.poidsVoisinage,
                       self.densiteToSeuil(Globals.densite))
        if not Globals.regles :
            Globals.regles = self.autoCell.regles
            self.resetRegles()


Main()


